﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using Microsoft.CSharp;
using System.Reflection;
using System.Globalization;

namespace SQLite2CELib
{
    public struct ViewCodeGeneratorInputs
    {
        public GeneratedLinqInfo LinqQuery { get; set; }
        public String NameOfView { get; set; }
        public String SqlMetalCodeAssemblyLoc { get; set; }
        public String ClassLocation { get; set; }
    }

    /// <summary>
    /// Class containing logic to generate the cs file for View statement in Sqlite.
    /// Inputs required are - 
    /// LINQ Equivalent of Sqlite View Query
    /// Assembly containing SqlMetal code
    /// Location where cs file should be generated
    /// Name of Sqlite View
    /// </summary>
    public class ViewCodeGenerator
    {
        private const string _bracingStyle = "C";
        //{0} = Name of the view.
        private const string _viewResultClassName = "{0}Result";

        private string[] _requiredImportStatements;
        private ViewCodeGeneratorInputs _input;

        public ViewCodeGenerator()
        {
            _requiredImportStatements = new string[] 
            {
              "using System;",
              "using System.Collections.Generic;",
              "using System.ComponentModel;",
              "using System.Data;",
              "using System.Data.Linq;",
              "using System.Data.Linq.Mapping;",
              "using System.Linq;",
              "using System.Linq.Expressions;",
              "using System.Reflection;"
            };
        }

        /// <summary>
        /// Method to generate the code that Simulates the Views in Sqlite to SqlCe.
        /// </summary>
        /// <param name="codeGenInputs">Input required by code gen</param>
        public void GenerateClass(ViewCodeGeneratorInputs codeGenInputs)
        {
            StringBuilder importStatements;
            StringBuilder classWithLinqQuery;
            String linqQueryWithLinqOutputDsName;

            _input = codeGenInputs;
            _input.NameOfView = codeGenInputs.NameOfView.Replace(" ", String.Empty);

            //Step 1. Prepare Import statements.
            importStatements = GetImportStatements();

            //Step 2. Update linq query and add Name of the ds class generated in step 2 after SELECT NEW {...} 
            //Output of the LINQ Query generator looks like
            //from eachCustomer in context.customers Select new { customerId = eachCustomer.CustomerId }
            //we have to append name of the dsClass after 'new' and before '{'
            linqQueryWithLinqOutputDsName = AppendLinqOutputDsToQuery(codeGenInputs.LinqQuery.Linq, _input.NameOfView);

            //Step 3. Prepare class that contains Properties corresponding to each column in select clause & method that retuns Linq Query Results.
            /* Class name - ViewName
             * This class conatins method namely GetResults which has - 
             * 1. context as input parameter
             * 2. IEnumerable<ViewResultDs> as output.
             */
            classWithLinqQuery = GenerateClassWithLinq(linqQueryWithLinqOutputDsName);

            //Step 4. Prepate .cs file containig import statements, ds and view class.
            SaveCodeToCsFile(importStatements, classWithLinqQuery);
        }

        private StringBuilder GetImportStatements()
        {
            StringBuilder importStatements;

            importStatements = new StringBuilder();
            foreach (String importStatement in _requiredImportStatements)
            {
                importStatements.Append(importStatement);
                importStatements.Append(Environment.NewLine);
            }
            return importStatements;
        }

        private String AppendLinqOutputDsToQuery(StringBuilder linq, string linqOutputDsClassName)
        {
            String selectNewClauseInLinq;
            String linqQueryWithLinqOutputDsName;
            int indexOfSelectNew;

            //Find the location of "select new " clause in linq query.
            selectNewClauseInLinq = "select new";
            linqQueryWithLinqOutputDsName = linq.ToString();
            indexOfSelectNew = linqQueryWithLinqOutputDsName.IndexOf(selectNewClauseInLinq, StringComparison.OrdinalIgnoreCase);

            while (indexOfSelectNew  != -1)
            {
                //Insert name of linq output data structure at the found index.
                linqQueryWithLinqOutputDsName = linqQueryWithLinqOutputDsName.Insert(indexOfSelectNew + selectNewClauseInLinq.Length, " " + linqOutputDsClassName + " ");
                indexOfSelectNew = linqQueryWithLinqOutputDsName.IndexOf(selectNewClauseInLinq, indexOfSelectNew + selectNewClauseInLinq.Length, StringComparison.OrdinalIgnoreCase);
            }

            return linqQueryWithLinqOutputDsName;
        }

        private StringBuilder GenerateClassWithLinq(string linqQueryWithLinqOutputDsName)
        {
            CodeTypeDeclaration viewClass;

            viewClass = new CodeTypeDeclaration(_input.NameOfView);

            //Add Properties corresponding to each column in select clause.
            AddPropertiesToClassWithLinq(ref viewClass);

            //Add method that embeds LINQ Query and returns IEnumerable.
            AddMethodWithLinqQuery(linqQueryWithLinqOutputDsName, ref viewClass);

            return GenerateCode(viewClass);
        }

        private void AddMethodWithLinqQuery(string linqQueryWithLinqOutputDsName, ref CodeTypeDeclaration viewClass)
        {
            CodeMemberMethod methodWithLinq;
            CodeSnippetExpression methodCode;
            CodeParameterDeclarationExpression dbContextParam;
            String returnTypeForMethodWithLinq;
            String dataContextClassName;
            String[] linesInLinqQuery;

            //Find the name of data context class using the assembly created using classes generated with SqlMetal.
            dataContextClassName = FindDataContextClassName();

            methodWithLinq = new CodeMemberMethod();
            methodWithLinq.Name = "GetResults";
            methodWithLinq.Attributes = MemberAttributes.Public | MemberAttributes.Final;

            returnTypeForMethodWithLinq = String.Concat("IEnumerable<", _input.NameOfView, ">");
            methodWithLinq.ReturnType = new CodeTypeReference(returnTypeForMethodWithLinq, CodeTypeReferenceOptions.GenericTypeParameter);

            dbContextParam = new CodeParameterDeclarationExpression();
            dbContextParam.Name = "context";
            dbContextParam.Type = new CodeTypeReference(dataContextClassName, CodeTypeReferenceOptions.GenericTypeParameter);
            methodWithLinq.Parameters.Add(dbContextParam);

            methodCode = new CodeSnippetExpression();

            linesInLinqQuery = linqQueryWithLinqOutputDsName.Split(new String[] { Environment.NewLine }, StringSplitOptions.None);
            //Add tabs to every line except first one.
            for (int i = 0; i < linesInLinqQuery.Count(); i++)
            {
                if (i == 0)
                {
                    methodCode.Value = String.Concat("return ", linesInLinqQuery[i]);
                    continue;
                }
                methodCode.Value += Environment.NewLine;
                methodCode.Value += String.Concat("\t\t", linesInLinqQuery[i]);
            }
            methodWithLinq.Statements.Add(methodCode);
            viewClass.Members.Add(methodWithLinq);
        }

        private void AddPropertiesToClassWithLinq(ref CodeTypeDeclaration viewClass)
        {
            CodeMemberField field;
            CodeMemberProperty property;

            foreach (KeyValuePair<String, PropertyDetails> requiredProperty in _input.LinqQuery.PropertyNameAndTypeInfo)
            {
                //Generate private field.
                field = new CodeMemberField();
                field.Type = new CodeTypeReference(requiredProperty.Value.TypeInfo);
                field.Name = String.Concat("_", requiredProperty.Key);
                viewClass.Members.Add(field);

                //Generate property for the field.
                property = new CodeMemberProperty();
                property.Type = new CodeTypeReference(requiredProperty.Value.TypeInfo);
                property.Name = requiredProperty.Key;
                property.HasGet = true;
                property.HasSet = true;
                property.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                property.GetStatements.Add(new CodeSnippetExpression(String.Concat("return ", field.Name)));
                property.SetStatements.Add(new CodeSnippetExpression(String.Concat(field.Name, " = value")));
                viewClass.Members.Add(property);
            }
        }

        private void SaveCodeToCsFile(StringBuilder importStatements, StringBuilder classWithLinqQuery)
        {
            String csFileLocation;
            FileStream csFileForView = null;
            StreamWriter csFileContentWriter = null;
            try
            {
                csFileLocation = Path.Combine(_input.ClassLocation, _input.NameOfView + ".cs");

                csFileForView = new FileStream(csFileLocation, FileMode.Create);
                csFileContentWriter = new StreamWriter(csFileForView);

                csFileContentWriter.WriteLine(importStatements.ToString());
                csFileContentWriter.WriteLine(classWithLinqQuery.ToString());
            }
            finally
            {
                if (csFileContentWriter != null)
                {
                    csFileContentWriter.Close();
                    csFileContentWriter.Dispose();
                }
                if (csFileForView != null)
                {
                    csFileForView.Close();
                    csFileForView.Dispose();
                }
            }
        }

        private string FindDataContextClassName()
        {
            String linqDataContextTypeName;
            Assembly linqToSqlClassesAssembly;
            Type[] classesInAssembly;
            Type dataContextClass = null;

            linqToSqlClassesAssembly = Assembly.LoadFrom(_input.SqlMetalCodeAssemblyLoc);
            classesInAssembly = linqToSqlClassesAssembly.GetTypes();

            linqDataContextTypeName = typeof(System.Data.Linq.DataContext).FullName;

            foreach (Type eachClassInAssembly in classesInAssembly)
            {
                if (eachClassInAssembly.BaseType != null &&
                    eachClassInAssembly.BaseType.FullName == linqDataContextTypeName)
                {
                    dataContextClass = eachClassInAssembly;
                }
            }
            return dataContextClass == null ? String.Empty : dataContextClass.Name;
        }

        private StringBuilder GenerateCode(CodeTypeDeclaration classToBeEmbedded)
        {
            CodeGeneratorOptions options;
            CodeDomProvider codeProvider;
            StringBuilder generatedCode;
            StringWriter codeWriter;

            options = new CodeGeneratorOptions();
            options.BracingStyle =  _bracingStyle;
            options.IndentString = "   ";

            codeProvider = new CSharpCodeProvider();

            generatedCode = new StringBuilder();
            codeWriter = new StringWriter(generatedCode, CultureInfo.InvariantCulture);

            codeProvider.GenerateCodeFromType(classToBeEmbedded, codeWriter, options);
            return generatedCode;
        }
    }
}
